feat(backend): resend org invitation — fresh token + extended TTL - #13603
feat(backend): resend org invitation — fresh token + extended TTL#13603ntindle wants to merge 7 commits into
Conversation
|
/batch |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds organization-scoped invitation resend support. The endpoint rotates tokens, refreshes expiration, reconciles team IDs, and validates invitation state. Listing can include expired invitations. HTTP tests and OpenAPI documentation cover the new behavior. ChangesInvitation resend
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant RequestContext
participant resend_invitation
participant Prisma
RequestContext->>resend_invitation: Authenticate and authorize request
resend_invitation->>Prisma: Load organization-scoped invitation
resend_invitation->>Prisma: Rotate token and refresh expiration
Prisma-->>resend_invitation: Return updated invitation
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Description checkExplanation The description clearly explains the invitation resend endpoint, expired invitation handling, authorization, token rotation, and tests. It also includes unrelated generated summary content, but the relevant content is sufficient. ✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🔍 PR Overlap DetectionThis check compares your PR against all other open PRs targeting the same branch to detect potential merge conflicts early. 🔴 Merge Conflicts DetectedThe following PRs have been tested and will have merge conflicts if merged after this PR. Consider coordinating with the authors.
🟡 Medium Risk — Some Line OverlapThese PRs have some overlapping changes:
🟢 Low Risk — File Overlap OnlyThese PRs touch the same files but different sections (click to expand)
Summary: 7 conflict(s), 5 medium risk, 7 low risk (out of 19 PRs with file overlap) Auto-generated on push. Ignores: |
|
🤖 Removed from the batch — this PR conflicts with the rest of the current group and could not be merged onto |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@autogpt_platform/backend/backend/api/features/orgs/invitation_routes.py`:
- Around line 137-179: Update resend_invitation’s prisma.orginvitation.update
call to catch the ORM not-found exception and translate it into NotFoundError
for invitation_id, including concurrent deletion cases. Remove the unreachable
refreshed-is-None check while preserving the existing response behavior for
successful updates.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: d5e765b1-f2f9-48e4-b544-083fd1911a78
📒 Files selected for processing (2)
autogpt_platform/backend/backend/api/features/orgs/invitation_routes.pyautogpt_platform/backend/backend/api/features/orgs/routes_test.py
📜 Review details
⏰ Context from checks skipped due to timeout. (14)
- GitHub Check: check API types
- GitHub Check: type-check (3.11)
- GitHub Check: type-check (3.12)
- GitHub Check: test (3.13)
- GitHub Check: type-check (3.13)
- GitHub Check: test (3.11)
- GitHub Check: lint
- GitHub Check: test (3.12)
- GitHub Check: types
- GitHub Check: Analyze (python)
- GitHub Check: lint
- GitHub Check: Analyze (typescript)
- GitHub Check: end-to-end tests
- GitHub Check: Check PR Status
🧰 Additional context used
📓 Path-based instructions (5)
autogpt_platform/backend/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
autogpt_platform/backend/**/*.py: Use Python 3.11 (required; managed by Poetry via pyproject.toml) for backend development
Always run 'poetry run format' (Black + isort) before linting in backend development
Always run 'poetry run lint' (ruff) after formatting in backend development
autogpt_platform/backend/**/*.py: Usepoetry run ...command for executing Python package dependencies
Use top-level imports only — avoid local/inner imports except for lazy imports of heavy optional dependencies likeopenpyxl
Use absolute imports withfrom backend.module import ...for cross-package imports; single-dot relative imports are acceptable for sibling modules within the same package; avoid double-dot relative imports
Do not use duck typing — avoidhasattr/getattr/isinstancefor type dispatch; use typed interfaces/unions/protocols instead
Use Pydantic models over dataclass/namedtuple/dict for structured data
Do not use linter suppressors — no# type: ignore,# noqa,# pyright: ignore; fix the type/code instead
Prefer list comprehensions over manual loop-and-append patterns
Use early return with guard clauses first to avoid deep nesting
Use%sfor deferred interpolation indebuglog statements for efficiency; use f-strings elsewhere for readability (e.g.,logger.debug("Processing %s items", count)vslogger.info(f"Processing {count} items"))
Sanitize error paths by usingos.path.basename()in error messages to avoid leaking directory structure
Be aware of TOCTOU (Time-Of-Check-Time-Of-Use) issues — avoid check-then-act patterns for file access and credit charging
Usetransaction=Truefor Redis pipelines to ensure atomicity on multi-step operations
Usemax(0, value)guards for computed values that should never be negative
Keep files under ~300 lines; if a file grows beyond this, split by responsibility (extract helpers, models, or a sub-module into a new file)
Keep functions under ~40 lines; extract named helpers when a function grows longer
...
Files:
autogpt_platform/backend/backend/api/features/orgs/invitation_routes.pyautogpt_platform/backend/backend/api/features/orgs/routes_test.py
autogpt_platform/backend/backend/api/features/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Update routes in '/backend/backend/api/features/' and add/update Pydantic models in the same directory for API development
Files:
autogpt_platform/backend/backend/api/features/orgs/invitation_routes.pyautogpt_platform/backend/backend/api/features/orgs/routes_test.py
autogpt_platform/{backend,autogpt_libs}/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
Format Python code with
poetry run format
Files:
autogpt_platform/backend/backend/api/features/orgs/invitation_routes.pyautogpt_platform/backend/backend/api/features/orgs/routes_test.py
autogpt_platform/backend/**/api/**/*.py
📄 CodeRabbit inference engine (autogpt_platform/backend/AGENTS.md)
autogpt_platform/backend/**/api/**/*.py: UseSecurity()instead ofDepends()for authentication dependencies to get proper OpenAPI security specification
Follow SSE (Server-Sent Events) protocol: usedata:lines for frontend-parsed events (must match Zod schema) and: commentlines for heartbeats/status
Files:
autogpt_platform/backend/backend/api/features/orgs/invitation_routes.pyautogpt_platform/backend/backend/api/features/orgs/routes_test.py
autogpt_platform/backend/**/*_test.py
📄 CodeRabbit inference engine (autogpt_platform/backend/AGENTS.md)
autogpt_platform/backend/**/*_test.py: Use pytest with snapshot testing for API responses
Colocate test files with source files using*_test.pynaming convention
Mock at boundaries — mock where the symbol is used, not where it's defined; after refactoring, update mock targets to match new module paths
UseAsyncMockfromunittest.mockfor async functions in tests
When writing tests, use Test-Driven Development (TDD): write failing tests marked with@pytest.mark.xfailbefore implementation, then remove the marker once the implementation is complete
When creating snapshots in tests, usepoetry run pytest path/to/test.py --snapshot-update; always review snapshot changes withgit diffbefore committing
Files:
autogpt_platform/backend/backend/api/features/orgs/routes_test.py
🧠 Learnings (11)
📚 Learning: 2026-02-26T17:02:22.448Z
Learnt from: Pwuts
Repo: Significant-Gravitas/AutoGPT PR: 12211
File: .pre-commit-config.yaml:160-179
Timestamp: 2026-02-26T17:02:22.448Z
Learning: Keep the pre-commit hook pattern broad for autogpt_platform/backend to ensure OpenAPI schema changes are captured. Do not narrow to backend/api/ alone, since the generated schema depends on Pydantic models across multiple directories (backend/data/, backend/blocks/, backend/copilot/, backend/integrations/, backend/util/). Narrowing could miss schema changes and cause frontend type desynchronization.
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_routes.pyautogpt_platform/backend/backend/api/features/orgs/routes_test.py
📚 Learning: 2026-03-05T15:42:08.207Z
Learnt from: ntindle
Repo: Significant-Gravitas/AutoGPT PR: 12297
File: .claude/skills/backend-check/SKILL.md:14-16
Timestamp: 2026-03-05T15:42:08.207Z
Learning: In Python files under autogpt_platform/backend (recursively), rely on poetry run format to perform formatting (Black + isort) and linting (ruff). Do not run poetry run lint as a separate step after poetry run format, since format already includes linting checks.
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_routes.pyautogpt_platform/backend/backend/api/features/orgs/routes_test.py
📚 Learning: 2026-03-16T16:35:40.236Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12440
File: autogpt_platform/backend/backend/api/features/workflow_import.py:54-63
Timestamp: 2026-03-16T16:35:40.236Z
Learning: Avoid using the word 'competitor' in public-facing identifiers and text. Use neutral naming for API paths, model names, function names, and UI text. Examples: rename 'CompetitorFormat' to 'SourcePlatform', 'convert_competitor_workflow' to 'convert_workflow', '/competitor-workflow' to '/workflow'. Apply this guideline to files under autogpt_platform/backend and autogpt_platform/frontend.
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_routes.pyautogpt_platform/backend/backend/api/features/orgs/routes_test.py
📚 Learning: 2026-03-31T15:37:38.626Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12623
File: autogpt_platform/backend/backend/copilot/tools/agent_generator/fixer.py:37-47
Timestamp: 2026-03-31T15:37:38.626Z
Learning: When validating/constructing Anthropic API model IDs in Significant-Gravitas/AutoGPT, allow the hyphen-separated Claude Opus 4.6 model ID `claude-opus-4-6` (it corresponds to `LlmModel.CLAUDE_4_6_OPUS` in `autogpt_platform/backend/backend/blocks/llm.py`). Do NOT require the dot-separated form in Anthropic contexts. Only OpenRouter routing variants should use the dot separator (e.g., `anthropic/claude-opus-4.6`); `claude-opus-4-6` should be treated as correct when passed to Anthropic, and flagged only if it’s used in the OpenRouter path where the dot form is expected.
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_routes.pyautogpt_platform/backend/backend/api/features/orgs/routes_test.py
📚 Learning: 2026-04-15T02:43:36.890Z
Learnt from: ntindle
Repo: Significant-Gravitas/AutoGPT PR: 12780
File: autogpt_platform/backend/backend/copilot/tools/workspace_files.py:0-0
Timestamp: 2026-04-15T02:43:36.890Z
Learning: When reviewing Python exception handlers, do not flag `isinstance(e, X)` checks as dead/unreachable if the caught exception `X` is a subclass of the exception type being handled. For example, if `X` (e.g., `VirusScanError`) inherits from `ValueError` (directly or via an intermediate class) and it can be raised within an `except ValueError:` block, then `isinstance(e, X)` inside that handler is reachable and should not be treated as dead code.
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_routes.pyautogpt_platform/backend/backend/api/features/orgs/routes_test.py
📚 Learning: 2026-05-23T05:29:43.085Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 13200
File: autogpt_platform/backend/backend/executor/scheduler.py:590-593
Timestamp: 2026-05-23T05:29:43.085Z
Learning: When reviewing Python code that uses Pydantic discriminated/tagged unions (e.g., `Annotated[Union[...], Field(discriminator="kind")]`), recognize that using `isinstance(x, SomeVariantInfo)` to narrow the union is an intentional and correct runtime guard and should also enable static type narrowing in tools like Pyright. Do not recommend replacing such `isinstance`-based narrowing with `cast(...)` when the check already proves the variant at runtime.
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_routes.pyautogpt_platform/backend/backend/api/features/orgs/routes_test.py
📚 Learning: 2026-04-22T11:46:04.431Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12881
File: autogpt_platform/backend/backend/copilot/config.py:0-0
Timestamp: 2026-04-22T11:46:04.431Z
Learning: Do not flag the Claude Sonnet 4.6 model ID as incorrect when it uses the project’s established hyphenated convention: `anthropic/claude-sonnet-4-6`. This hyphen form is the intentional, production convention and should be treated as valid (including in files like llm.py, blocks tests, reasoning.py, `_is_anthropic_model` tests, and config defaults). Note that OpenRouter also accepts the dot variant `anthropic/claude-sonnet-4.6`, so either form may be tolerated, but `anthropic/claude-sonnet-4-6` should be considered the standard to match project usage.
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_routes.pyautogpt_platform/backend/backend/api/features/orgs/routes_test.py
📚 Learning: 2026-04-22T11:46:12.892Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12881
File: autogpt_platform/backend/backend/copilot/baseline/service.py:322-332
Timestamp: 2026-04-22T11:46:12.892Z
Learning: In this codebase (Significant-Gravitas/AutoGPT), OpenRouter-routed Anthropic model IDs should use the hyphen-separated convention (e.g., `anthropic/claude-sonnet-4-6`, `anthropic/claude-opus-4-6`). Although OpenRouter may accept both hyphen and dot variants, treat the hyphen-separated form as the intended, correct codebase-wide convention and do not flag it as an error. Only flag the dot-separated variant (e.g., `anthropic/claude-sonnet-4.6`) as incorrect when reviewing/validating model ID strings for OpenRouter-routed Anthropic models.
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_routes.pyautogpt_platform/backend/backend/api/features/orgs/routes_test.py
📚 Learning: 2026-05-07T18:48:14.242Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 13040
File: autogpt_platform/backend/backend/blocks/llm.py:0-0
Timestamp: 2026-05-07T18:48:14.242Z
Learning: In this repository, isort may split imports from the same module into separate blocks when some imports are aliased (e.g., `from module import X as Y`) and others are not. Preserve the two-block layout when it results from isort (such as keeping `from openai.types.chat import ChatCompletion as OpenAIChatCompletion` separate from non-aliased imports from `openai.types.chat`). Do not treat that split as a style issue during review; merging them into a single block can fail CI with `Imports are incorrectly sorted and/or formatted`.
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_routes.pyautogpt_platform/backend/backend/api/features/orgs/routes_test.py
📚 Learning: 2026-05-26T14:24:34.866Z
Learnt from: Abhi1992002
Repo: Significant-Gravitas/AutoGPT PR: 13217
File: autogpt_platform/backend/backend/api/features/search/service.py:137-137
Timestamp: 2026-05-26T14:24:34.866Z
Learning: In the Significant-Gravitas/AutoGPT backend, treat `user_id` (an opaque UUID used only for correlation/tracing) as non-PII. Do not flag direct logging of `user_id` in `logger.warning`/`logger.info` statements as a PII exposure issue, as the established convention is to log `user_id` for tracing while reserving PII for fields like email or display name.
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_routes.pyautogpt_platform/backend/backend/api/features/orgs/routes_test.py
📚 Learning: 2026-06-11T19:39:10.493Z
Learnt from: ntindle
Repo: Significant-Gravitas/AutoGPT PR: 13337
File: autogpt_platform/backend/backend/copilot/graphiti/reranker.py:0-0
Timestamp: 2026-06-11T19:39:10.493Z
Learning: In the Significant-Gravitas/AutoGPT Python backend, when calling the OpenAI Python client `chat.completions.create`, construct the `messages` payload using the concrete typed-dict variants from `openai.types.chat` (e.g., `ChatCompletionSystemMessageParam`, `ChatCompletionUserMessageParam`, etc.) rather than trying to instantiate `ChatCompletionMessageParam` directly. `ChatCompletionMessageParam` is a `Union` alias and is not constructible, so `ChatCompletionMessageParam(role=..., content=...)` should fail type checking. Build each message element with the appropriate concrete typed dict and then annotate the resulting list as `list[ChatCompletionMessageParam]` (e.g., `messages: list[ChatCompletionMessageParam] = [ChatCompletionSystemMessageParam(...), ...]`).
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_routes.pyautogpt_platform/backend/backend/api/features/orgs/routes_test.py
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## feat/grant-credential-modes #13603 +/- ##
===============================================================
+ Coverage 80.53% 80.55% +0.01%
===============================================================
Files 3334 3335 +1
Lines 256126 256343 +217
Branches 23633 23643 +10
===============================================================
+ Hits 206280 206501 +221
+ Misses 44551 44534 -17
- Partials 5295 5308 +13
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
|
/batch |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.py`:
- Around line 77-85: Move the local imports from the _client method to the
module-level imports, adding get_request_context and org_router there. Keep
_client focused on creating the FastAPI app, including org_router, and
registering the dependency override without changing its behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: ffbd938a-b471-4dbf-9a29-ce1666561732
📒 Files selected for processing (1)
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.py
📜 Review details
⏰ Context from checks skipped due to timeout. (15)
- GitHub Check: check API types
- GitHub Check: Cursor Bugbot
- GitHub Check: Check PR Status
- GitHub Check: Analyze (python)
- GitHub Check: Analyze (typescript)
- GitHub Check: end-to-end tests
- GitHub Check: types
- GitHub Check: lint
- GitHub Check: test (3.12)
- GitHub Check: test (3.13)
- GitHub Check: type-check (3.13)
- GitHub Check: type-check (3.12)
- GitHub Check: test (3.11)
- GitHub Check: type-check (3.11)
- GitHub Check: lint
🧰 Additional context used
📓 Path-based instructions (5)
autogpt_platform/backend/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
autogpt_platform/backend/**/*.py: Use Python 3.11 (required; managed by Poetry via pyproject.toml) for backend development
Always run 'poetry run format' (Black + isort) before linting in backend development
Always run 'poetry run lint' (ruff) after formatting in backend development
autogpt_platform/backend/**/*.py: Usepoetry run ...command for executing Python package dependencies
Use top-level imports only — avoid local/inner imports except for lazy imports of heavy optional dependencies likeopenpyxl
Use absolute imports withfrom backend.module import ...for cross-package imports; single-dot relative imports are acceptable for sibling modules within the same package; avoid double-dot relative imports
Do not use duck typing — avoidhasattr/getattr/isinstancefor type dispatch; use typed interfaces/unions/protocols instead
Use Pydantic models over dataclass/namedtuple/dict for structured data
Do not use linter suppressors — no# type: ignore,# noqa,# pyright: ignore; fix the type/code instead
Prefer list comprehensions over manual loop-and-append patterns
Use early return with guard clauses first to avoid deep nesting
Use%sfor deferred interpolation indebuglog statements for efficiency; use f-strings elsewhere for readability (e.g.,logger.debug("Processing %s items", count)vslogger.info(f"Processing {count} items"))
Sanitize error paths by usingos.path.basename()in error messages to avoid leaking directory structure
Be aware of TOCTOU (Time-Of-Check-Time-Of-Use) issues — avoid check-then-act patterns for file access and credit charging
Usetransaction=Truefor Redis pipelines to ensure atomicity on multi-step operations
Usemax(0, value)guards for computed values that should never be negative
Keep files under ~300 lines; if a file grows beyond this, split by responsibility (extract helpers, models, or a sub-module into a new file)
Keep functions under ~40 lines; extract named helpers when a function grows longer
...
Files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.py
autogpt_platform/backend/backend/api/features/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Update routes in '/backend/backend/api/features/' and add/update Pydantic models in the same directory for API development
Files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.py
autogpt_platform/{backend,autogpt_libs}/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
Format Python code with
poetry run format
Files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.py
autogpt_platform/backend/**/api/**/*.py
📄 CodeRabbit inference engine (autogpt_platform/backend/AGENTS.md)
autogpt_platform/backend/**/api/**/*.py: UseSecurity()instead ofDepends()for authentication dependencies to get proper OpenAPI security specification
Follow SSE (Server-Sent Events) protocol: usedata:lines for frontend-parsed events (must match Zod schema) and: commentlines for heartbeats/status
Files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.py
autogpt_platform/backend/**/*_test.py
📄 CodeRabbit inference engine (autogpt_platform/backend/AGENTS.md)
autogpt_platform/backend/**/*_test.py: Use pytest with snapshot testing for API responses
Colocate test files with source files using*_test.pynaming convention
Mock at boundaries — mock where the symbol is used, not where it's defined; after refactoring, update mock targets to match new module paths
UseAsyncMockfromunittest.mockfor async functions in tests
When writing tests, use Test-Driven Development (TDD): write failing tests marked with@pytest.mark.xfailbefore implementation, then remove the marker once the implementation is complete
When creating snapshots in tests, usepoetry run pytest path/to/test.py --snapshot-update; always review snapshot changes withgit diffbefore committing
Files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.py
🧠 Learnings (11)
📚 Learning: 2026-02-26T17:02:22.448Z
Learnt from: Pwuts
Repo: Significant-Gravitas/AutoGPT PR: 12211
File: .pre-commit-config.yaml:160-179
Timestamp: 2026-02-26T17:02:22.448Z
Learning: Keep the pre-commit hook pattern broad for autogpt_platform/backend to ensure OpenAPI schema changes are captured. Do not narrow to backend/api/ alone, since the generated schema depends on Pydantic models across multiple directories (backend/data/, backend/blocks/, backend/copilot/, backend/integrations/, backend/util/). Narrowing could miss schema changes and cause frontend type desynchronization.
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.py
📚 Learning: 2026-03-05T15:42:08.207Z
Learnt from: ntindle
Repo: Significant-Gravitas/AutoGPT PR: 12297
File: .claude/skills/backend-check/SKILL.md:14-16
Timestamp: 2026-03-05T15:42:08.207Z
Learning: In Python files under autogpt_platform/backend (recursively), rely on poetry run format to perform formatting (Black + isort) and linting (ruff). Do not run poetry run lint as a separate step after poetry run format, since format already includes linting checks.
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.py
📚 Learning: 2026-03-16T16:35:40.236Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12440
File: autogpt_platform/backend/backend/api/features/workflow_import.py:54-63
Timestamp: 2026-03-16T16:35:40.236Z
Learning: Avoid using the word 'competitor' in public-facing identifiers and text. Use neutral naming for API paths, model names, function names, and UI text. Examples: rename 'CompetitorFormat' to 'SourcePlatform', 'convert_competitor_workflow' to 'convert_workflow', '/competitor-workflow' to '/workflow'. Apply this guideline to files under autogpt_platform/backend and autogpt_platform/frontend.
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.py
📚 Learning: 2026-03-31T15:37:38.626Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12623
File: autogpt_platform/backend/backend/copilot/tools/agent_generator/fixer.py:37-47
Timestamp: 2026-03-31T15:37:38.626Z
Learning: When validating/constructing Anthropic API model IDs in Significant-Gravitas/AutoGPT, allow the hyphen-separated Claude Opus 4.6 model ID `claude-opus-4-6` (it corresponds to `LlmModel.CLAUDE_4_6_OPUS` in `autogpt_platform/backend/backend/blocks/llm.py`). Do NOT require the dot-separated form in Anthropic contexts. Only OpenRouter routing variants should use the dot separator (e.g., `anthropic/claude-opus-4.6`); `claude-opus-4-6` should be treated as correct when passed to Anthropic, and flagged only if it’s used in the OpenRouter path where the dot form is expected.
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.py
📚 Learning: 2026-04-15T02:43:36.890Z
Learnt from: ntindle
Repo: Significant-Gravitas/AutoGPT PR: 12780
File: autogpt_platform/backend/backend/copilot/tools/workspace_files.py:0-0
Timestamp: 2026-04-15T02:43:36.890Z
Learning: When reviewing Python exception handlers, do not flag `isinstance(e, X)` checks as dead/unreachable if the caught exception `X` is a subclass of the exception type being handled. For example, if `X` (e.g., `VirusScanError`) inherits from `ValueError` (directly or via an intermediate class) and it can be raised within an `except ValueError:` block, then `isinstance(e, X)` inside that handler is reachable and should not be treated as dead code.
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.py
📚 Learning: 2026-05-23T05:29:43.085Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 13200
File: autogpt_platform/backend/backend/executor/scheduler.py:590-593
Timestamp: 2026-05-23T05:29:43.085Z
Learning: When reviewing Python code that uses Pydantic discriminated/tagged unions (e.g., `Annotated[Union[...], Field(discriminator="kind")]`), recognize that using `isinstance(x, SomeVariantInfo)` to narrow the union is an intentional and correct runtime guard and should also enable static type narrowing in tools like Pyright. Do not recommend replacing such `isinstance`-based narrowing with `cast(...)` when the check already proves the variant at runtime.
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.py
📚 Learning: 2026-04-22T11:46:04.431Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12881
File: autogpt_platform/backend/backend/copilot/config.py:0-0
Timestamp: 2026-04-22T11:46:04.431Z
Learning: Do not flag the Claude Sonnet 4.6 model ID as incorrect when it uses the project’s established hyphenated convention: `anthropic/claude-sonnet-4-6`. This hyphen form is the intentional, production convention and should be treated as valid (including in files like llm.py, blocks tests, reasoning.py, `_is_anthropic_model` tests, and config defaults). Note that OpenRouter also accepts the dot variant `anthropic/claude-sonnet-4.6`, so either form may be tolerated, but `anthropic/claude-sonnet-4-6` should be considered the standard to match project usage.
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.py
📚 Learning: 2026-04-22T11:46:12.892Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12881
File: autogpt_platform/backend/backend/copilot/baseline/service.py:322-332
Timestamp: 2026-04-22T11:46:12.892Z
Learning: In this codebase (Significant-Gravitas/AutoGPT), OpenRouter-routed Anthropic model IDs should use the hyphen-separated convention (e.g., `anthropic/claude-sonnet-4-6`, `anthropic/claude-opus-4-6`). Although OpenRouter may accept both hyphen and dot variants, treat the hyphen-separated form as the intended, correct codebase-wide convention and do not flag it as an error. Only flag the dot-separated variant (e.g., `anthropic/claude-sonnet-4.6`) as incorrect when reviewing/validating model ID strings for OpenRouter-routed Anthropic models.
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.py
📚 Learning: 2026-05-07T18:48:14.242Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 13040
File: autogpt_platform/backend/backend/blocks/llm.py:0-0
Timestamp: 2026-05-07T18:48:14.242Z
Learning: In this repository, isort may split imports from the same module into separate blocks when some imports are aliased (e.g., `from module import X as Y`) and others are not. Preserve the two-block layout when it results from isort (such as keeping `from openai.types.chat import ChatCompletion as OpenAIChatCompletion` separate from non-aliased imports from `openai.types.chat`). Do not treat that split as a style issue during review; merging them into a single block can fail CI with `Imports are incorrectly sorted and/or formatted`.
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.py
📚 Learning: 2026-05-26T14:24:34.866Z
Learnt from: Abhi1992002
Repo: Significant-Gravitas/AutoGPT PR: 13217
File: autogpt_platform/backend/backend/api/features/search/service.py:137-137
Timestamp: 2026-05-26T14:24:34.866Z
Learning: In the Significant-Gravitas/AutoGPT backend, treat `user_id` (an opaque UUID used only for correlation/tracing) as non-PII. Do not flag direct logging of `user_id` in `logger.warning`/`logger.info` statements as a PII exposure issue, as the established convention is to log `user_id` for tracing while reserving PII for fields like email or display name.
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.py
📚 Learning: 2026-06-11T19:39:10.493Z
Learnt from: ntindle
Repo: Significant-Gravitas/AutoGPT PR: 13337
File: autogpt_platform/backend/backend/copilot/graphiti/reranker.py:0-0
Timestamp: 2026-06-11T19:39:10.493Z
Learning: In the Significant-Gravitas/AutoGPT Python backend, when calling the OpenAI Python client `chat.completions.create`, construct the `messages` payload using the concrete typed-dict variants from `openai.types.chat` (e.g., `ChatCompletionSystemMessageParam`, `ChatCompletionUserMessageParam`, etc.) rather than trying to instantiate `ChatCompletionMessageParam` directly. `ChatCompletionMessageParam` is a `Union` alias and is not constructible, so `ChatCompletionMessageParam(role=..., content=...)` should fail type checking. Build each message element with the appropriate concrete typed dict and then annotate the resulting list as `list[ChatCompletionMessageParam]` (e.g., `messages: list[ChatCompletionMessageParam] = [ChatCompletionSystemMessageParam(...), ...]`).
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.py
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.py`:
- Around line 159-166: Update the resend test around the
_client(_owner_ctx()).post call to normalize volatile timestamp fields in the
complete JSON response, then snapshot the normalized response using the
project’s pytest snapshot convention. Retain both existing token assertions
alongside the new full-response snapshot.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 9f08fe7e-141b-4125-b4e5-1c048d86ba27
📒 Files selected for processing (2)
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.pyautogpt_platform/backend/backend/api/features/orgs/invitation_routes.py
📜 Review details
⏰ Context from checks skipped due to timeout. (16)
- GitHub Check: setup
- GitHub Check: end-to-end tests
- GitHub Check: type-check (3.13)
- GitHub Check: test (3.11)
- GitHub Check: test (3.12)
- GitHub Check: lint
- GitHub Check: test (3.13)
- GitHub Check: type-check (3.12)
- GitHub Check: type-check (3.11)
- GitHub Check: setup
- GitHub Check: Seer Code Review
- GitHub Check: Check PR Status
- GitHub Check: types
- GitHub Check: lint
- GitHub Check: Analyze (python)
- GitHub Check: Analyze (typescript)
🧰 Additional context used
📓 Path-based instructions (5)
autogpt_platform/backend/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
autogpt_platform/backend/**/*.py: Use Python 3.11 (required; managed by Poetry via pyproject.toml) for backend development
Always run 'poetry run format' (Black + isort) before linting in backend development
Always run 'poetry run lint' (ruff) after formatting in backend development
autogpt_platform/backend/**/*.py: Usepoetry run ...command for executing Python package dependencies
Use top-level imports only — avoid local/inner imports except for lazy imports of heavy optional dependencies likeopenpyxl
Use absolute imports withfrom backend.module import ...for cross-package imports; single-dot relative imports are acceptable for sibling modules within the same package; avoid double-dot relative imports
Do not use duck typing — avoidhasattr/getattr/isinstancefor type dispatch; use typed interfaces/unions/protocols instead
Use Pydantic models over dataclass/namedtuple/dict for structured data
Do not use linter suppressors — no# type: ignore,# noqa,# pyright: ignore; fix the type/code instead
Prefer list comprehensions over manual loop-and-append patterns
Use early return with guard clauses first to avoid deep nesting
Use%sfor deferred interpolation indebuglog statements for efficiency; use f-strings elsewhere for readability (e.g.,logger.debug("Processing %s items", count)vslogger.info(f"Processing {count} items"))
Sanitize error paths by usingos.path.basename()in error messages to avoid leaking directory structure
Be aware of TOCTOU (Time-Of-Check-Time-Of-Use) issues — avoid check-then-act patterns for file access and credit charging
Usetransaction=Truefor Redis pipelines to ensure atomicity on multi-step operations
Usemax(0, value)guards for computed values that should never be negative
Keep files under ~300 lines; if a file grows beyond this, split by responsibility (extract helpers, models, or a sub-module into a new file)
Keep functions under ~40 lines; extract named helpers when a function grows longer
...
Files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.pyautogpt_platform/backend/backend/api/features/orgs/invitation_routes.py
autogpt_platform/backend/backend/api/features/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Update routes in '/backend/backend/api/features/' and add/update Pydantic models in the same directory for API development
Files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.pyautogpt_platform/backend/backend/api/features/orgs/invitation_routes.py
autogpt_platform/{backend,autogpt_libs}/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
Format Python code with
poetry run format
Files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.pyautogpt_platform/backend/backend/api/features/orgs/invitation_routes.py
autogpt_platform/backend/**/api/**/*.py
📄 CodeRabbit inference engine (autogpt_platform/backend/AGENTS.md)
autogpt_platform/backend/**/api/**/*.py: UseSecurity()instead ofDepends()for authentication dependencies to get proper OpenAPI security specification
Follow SSE (Server-Sent Events) protocol: usedata:lines for frontend-parsed events (must match Zod schema) and: commentlines for heartbeats/status
Files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.pyautogpt_platform/backend/backend/api/features/orgs/invitation_routes.py
autogpt_platform/backend/**/*_test.py
📄 CodeRabbit inference engine (autogpt_platform/backend/AGENTS.md)
autogpt_platform/backend/**/*_test.py: Use pytest with snapshot testing for API responses
Colocate test files with source files using*_test.pynaming convention
Mock at boundaries — mock where the symbol is used, not where it's defined; after refactoring, update mock targets to match new module paths
UseAsyncMockfromunittest.mockfor async functions in tests
When writing tests, use Test-Driven Development (TDD): write failing tests marked with@pytest.mark.xfailbefore implementation, then remove the marker once the implementation is complete
When creating snapshots in tests, usepoetry run pytest path/to/test.py --snapshot-update; always review snapshot changes withgit diffbefore committing
Files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.py
🧠 Learnings (11)
📚 Learning: 2026-02-26T17:02:22.448Z
Learnt from: Pwuts
Repo: Significant-Gravitas/AutoGPT PR: 12211
File: .pre-commit-config.yaml:160-179
Timestamp: 2026-02-26T17:02:22.448Z
Learning: Keep the pre-commit hook pattern broad for autogpt_platform/backend to ensure OpenAPI schema changes are captured. Do not narrow to backend/api/ alone, since the generated schema depends on Pydantic models across multiple directories (backend/data/, backend/blocks/, backend/copilot/, backend/integrations/, backend/util/). Narrowing could miss schema changes and cause frontend type desynchronization.
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.pyautogpt_platform/backend/backend/api/features/orgs/invitation_routes.py
📚 Learning: 2026-03-05T15:42:08.207Z
Learnt from: ntindle
Repo: Significant-Gravitas/AutoGPT PR: 12297
File: .claude/skills/backend-check/SKILL.md:14-16
Timestamp: 2026-03-05T15:42:08.207Z
Learning: In Python files under autogpt_platform/backend (recursively), rely on poetry run format to perform formatting (Black + isort) and linting (ruff). Do not run poetry run lint as a separate step after poetry run format, since format already includes linting checks.
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.pyautogpt_platform/backend/backend/api/features/orgs/invitation_routes.py
📚 Learning: 2026-03-16T16:35:40.236Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12440
File: autogpt_platform/backend/backend/api/features/workflow_import.py:54-63
Timestamp: 2026-03-16T16:35:40.236Z
Learning: Avoid using the word 'competitor' in public-facing identifiers and text. Use neutral naming for API paths, model names, function names, and UI text. Examples: rename 'CompetitorFormat' to 'SourcePlatform', 'convert_competitor_workflow' to 'convert_workflow', '/competitor-workflow' to '/workflow'. Apply this guideline to files under autogpt_platform/backend and autogpt_platform/frontend.
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.pyautogpt_platform/backend/backend/api/features/orgs/invitation_routes.py
📚 Learning: 2026-03-31T15:37:38.626Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12623
File: autogpt_platform/backend/backend/copilot/tools/agent_generator/fixer.py:37-47
Timestamp: 2026-03-31T15:37:38.626Z
Learning: When validating/constructing Anthropic API model IDs in Significant-Gravitas/AutoGPT, allow the hyphen-separated Claude Opus 4.6 model ID `claude-opus-4-6` (it corresponds to `LlmModel.CLAUDE_4_6_OPUS` in `autogpt_platform/backend/backend/blocks/llm.py`). Do NOT require the dot-separated form in Anthropic contexts. Only OpenRouter routing variants should use the dot separator (e.g., `anthropic/claude-opus-4.6`); `claude-opus-4-6` should be treated as correct when passed to Anthropic, and flagged only if it’s used in the OpenRouter path where the dot form is expected.
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.pyautogpt_platform/backend/backend/api/features/orgs/invitation_routes.py
📚 Learning: 2026-04-15T02:43:36.890Z
Learnt from: ntindle
Repo: Significant-Gravitas/AutoGPT PR: 12780
File: autogpt_platform/backend/backend/copilot/tools/workspace_files.py:0-0
Timestamp: 2026-04-15T02:43:36.890Z
Learning: When reviewing Python exception handlers, do not flag `isinstance(e, X)` checks as dead/unreachable if the caught exception `X` is a subclass of the exception type being handled. For example, if `X` (e.g., `VirusScanError`) inherits from `ValueError` (directly or via an intermediate class) and it can be raised within an `except ValueError:` block, then `isinstance(e, X)` inside that handler is reachable and should not be treated as dead code.
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.pyautogpt_platform/backend/backend/api/features/orgs/invitation_routes.py
📚 Learning: 2026-05-23T05:29:43.085Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 13200
File: autogpt_platform/backend/backend/executor/scheduler.py:590-593
Timestamp: 2026-05-23T05:29:43.085Z
Learning: When reviewing Python code that uses Pydantic discriminated/tagged unions (e.g., `Annotated[Union[...], Field(discriminator="kind")]`), recognize that using `isinstance(x, SomeVariantInfo)` to narrow the union is an intentional and correct runtime guard and should also enable static type narrowing in tools like Pyright. Do not recommend replacing such `isinstance`-based narrowing with `cast(...)` when the check already proves the variant at runtime.
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.pyautogpt_platform/backend/backend/api/features/orgs/invitation_routes.py
📚 Learning: 2026-04-22T11:46:04.431Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12881
File: autogpt_platform/backend/backend/copilot/config.py:0-0
Timestamp: 2026-04-22T11:46:04.431Z
Learning: Do not flag the Claude Sonnet 4.6 model ID as incorrect when it uses the project’s established hyphenated convention: `anthropic/claude-sonnet-4-6`. This hyphen form is the intentional, production convention and should be treated as valid (including in files like llm.py, blocks tests, reasoning.py, `_is_anthropic_model` tests, and config defaults). Note that OpenRouter also accepts the dot variant `anthropic/claude-sonnet-4.6`, so either form may be tolerated, but `anthropic/claude-sonnet-4-6` should be considered the standard to match project usage.
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.pyautogpt_platform/backend/backend/api/features/orgs/invitation_routes.py
📚 Learning: 2026-04-22T11:46:12.892Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12881
File: autogpt_platform/backend/backend/copilot/baseline/service.py:322-332
Timestamp: 2026-04-22T11:46:12.892Z
Learning: In this codebase (Significant-Gravitas/AutoGPT), OpenRouter-routed Anthropic model IDs should use the hyphen-separated convention (e.g., `anthropic/claude-sonnet-4-6`, `anthropic/claude-opus-4-6`). Although OpenRouter may accept both hyphen and dot variants, treat the hyphen-separated form as the intended, correct codebase-wide convention and do not flag it as an error. Only flag the dot-separated variant (e.g., `anthropic/claude-sonnet-4.6`) as incorrect when reviewing/validating model ID strings for OpenRouter-routed Anthropic models.
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.pyautogpt_platform/backend/backend/api/features/orgs/invitation_routes.py
📚 Learning: 2026-05-07T18:48:14.242Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 13040
File: autogpt_platform/backend/backend/blocks/llm.py:0-0
Timestamp: 2026-05-07T18:48:14.242Z
Learning: In this repository, isort may split imports from the same module into separate blocks when some imports are aliased (e.g., `from module import X as Y`) and others are not. Preserve the two-block layout when it results from isort (such as keeping `from openai.types.chat import ChatCompletion as OpenAIChatCompletion` separate from non-aliased imports from `openai.types.chat`). Do not treat that split as a style issue during review; merging them into a single block can fail CI with `Imports are incorrectly sorted and/or formatted`.
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.pyautogpt_platform/backend/backend/api/features/orgs/invitation_routes.py
📚 Learning: 2026-05-26T14:24:34.866Z
Learnt from: Abhi1992002
Repo: Significant-Gravitas/AutoGPT PR: 13217
File: autogpt_platform/backend/backend/api/features/search/service.py:137-137
Timestamp: 2026-05-26T14:24:34.866Z
Learning: In the Significant-Gravitas/AutoGPT backend, treat `user_id` (an opaque UUID used only for correlation/tracing) as non-PII. Do not flag direct logging of `user_id` in `logger.warning`/`logger.info` statements as a PII exposure issue, as the established convention is to log `user_id` for tracing while reserving PII for fields like email or display name.
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.pyautogpt_platform/backend/backend/api/features/orgs/invitation_routes.py
📚 Learning: 2026-06-11T19:39:10.493Z
Learnt from: ntindle
Repo: Significant-Gravitas/AutoGPT PR: 13337
File: autogpt_platform/backend/backend/copilot/graphiti/reranker.py:0-0
Timestamp: 2026-06-11T19:39:10.493Z
Learning: In the Significant-Gravitas/AutoGPT Python backend, when calling the OpenAI Python client `chat.completions.create`, construct the `messages` payload using the concrete typed-dict variants from `openai.types.chat` (e.g., `ChatCompletionSystemMessageParam`, `ChatCompletionUserMessageParam`, etc.) rather than trying to instantiate `ChatCompletionMessageParam` directly. `ChatCompletionMessageParam` is a `Union` alias and is not constructible, so `ChatCompletionMessageParam(role=..., content=...)` should fail type checking. Build each message element with the appropriate concrete typed dict and then annotate the resulting list as `list[ChatCompletionMessageParam]` (e.g., `messages: list[ChatCompletionMessageParam] = [ChatCompletionSystemMessageParam(...), ...]`).
Applied to files:
autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.pyautogpt_platform/backend/backend/api/features/orgs/invitation_routes.py
🔇 Additional comments (2)
autogpt_platform/backend/backend/api/features/orgs/invitation_routes.py (1)
212-218: LGTM!autogpt_platform/backend/backend/api/features/orgs/invitation_resend_test.py (1)
136-150: LGTM!
|
👋 Friendly reminder: This PR is waiting on a signed CLA. All contributors need to sign our Contributor License Agreement before we can merge this PR. Why do we need a CLA?The CLA protects both you and the project by clarifying the terms under which your contribution is made. It's a one-time process — once signed, it covers all your future contributions. Common issues
If you have questions, just ask! 🙂 |
|
/review |
There was a problem hiding this comment.
❓ INCONCLUSIVE
You've hit your session limit · resets 9am (UTC)
Risk level: medium | Human review: recommended | Duration: 932s | Reviewed: 25d880f4
Specialist Reports
| Specialist | Status | Summary |
|---|---|---|
| security | You've hit your session limit · resets 9am (UTC) | |
| architect | You've hit your session limit · resets 9am (UTC) | |
| performance | You've hit your session limit · resets 9am (UTC) | |
| testing | You've hit your session limit · resets 9am (UTC) | |
| quality | You've hit your session limit · resets 9am (UTC) | |
| product | You've hit your session limit · resets 9am (UTC) | |
| discussion | You've hit your session limit · resets 9am (UTC) | |
| ui-reviewer (local) | You've hit your session limit · resets 9am (UTC) | |
| ui-reviewer (hosted) | ✅ PASS | Resend endpoint verified live end-to-end: token rotation, TTL extension, expired-invite recovery via include_expired, and full 400/401/403/404 authz matrix all pass with no server errors. |
Findings: 🔴 0 critical | 🟠 0 high | 🟡 0 medium | 🟢 1 low
Should Fix
- 🟢
autogpt_platform/frontend/src/app/api/openapi.json:9721operationId 'postV2Resend invitation' contains a space and the tags array is duplicated (['v2','orgs','invitations','orgs','invitations']), which can produce awkward generated Orval hook names for this route.
Suggestion: Give the route an explicit clean operation_id (e.g. resend_invitation) so the generated hook name is well-formed; dedupe the tags list.
Quality Checks
- ✅ lint: cd autogpt_platform/frontend && pnpm lint:
cd autogpt_platform/frontend && pnpm lint(80s) - ✅ lint: cd autogpt_platform/backend && poetry run lint:
cd autogpt_platform/backend && poetry run lint(103s) - ✅ typecheck: cd autogpt_platform/frontend && pnpm types:
cd autogpt_platform/frontend && pnpm types(51s) - ❌ test: cd autogpt_platform/frontend && mv .env /tmp/qa-env-stash 2>/dev/null; pnpm test:unit; rc=$?; [ -f /tmp/qa-env-stash ] && mv /tmp/qa-env-stash .env; exit $rc:
cd autogpt_platform/frontend && mv .env /tmp/qa-env-stash 2>/dev/null; pnpm test:unit; rc=$?; [ -f /tmp/qa-env-stash ] && mv /tmp/qa-env-stash .env; exit $rc(393s) - ✅ build: cd autogpt_platform/frontend && pnpm build:
cd autogpt_platform/frontend && pnpm build(294s)
| "/api/orgs/{org_id}/invitations/{invitation_id}/resend": { | ||
| "post": { | ||
| "tags": ["v2", "orgs", "invitations", "orgs", "invitations"], | ||
| "summary": "Resend invitation", |
There was a problem hiding this comment.
🤖 🟢 low (ui-reviewer/openapi-codegen)
operationId 'postV2Resend invitation' contains a space and the tags array is duplicated (['v2','orgs','invitations','orgs','invitations']), which can produce awkward generated Orval hook names for this route.
Suggestion: Give the route an explicit clean operation_id (e.g. resend_invitation) so the generated hook name is well-formed; dedupe the tags list.
Superseded by a newer automated review.
There was a problem hiding this comment.
❓ INCONCLUSIVE
You've hit your session limit · resets 9am (UTC)
Risk level: medium | Human review: recommended | Duration: 857s | Reviewed: a9aca9e9
Specialist Reports
| Specialist | Status | Summary |
|---|---|---|
| security | You've hit your session limit · resets 9am (UTC) | |
| architect | You've hit your session limit · resets 9am (UTC) | |
| performance | You've hit your session limit · resets 9am (UTC) | |
| testing | You've hit your session limit · resets 9am (UTC) | |
| quality | You've hit your session limit · resets 9am (UTC) | |
| product | You've hit your session limit · resets 9am (UTC) | |
| discussion | You've hit your session limit · resets 9am (UTC) | |
| ui-reviewer (local) | You've hit your session limit · resets 9am (UTC) | |
| ui-reviewer (hosted) | ❌ FAIL | I'll start with the mandatory environment setup and auth. Token is empty. Let me try sign-up fallback and re-auth. No users exist — DB is fresh. Let me sign up a user with a valid password. |
Quality Checks
- ✅ lint: cd autogpt_platform/frontend && pnpm lint:
cd autogpt_platform/frontend && pnpm lint(75s) - ✅ lint: cd autogpt_platform/backend && poetry run lint:
cd autogpt_platform/backend && poetry run lint(96s) - ✅ typecheck: cd autogpt_platform/frontend && pnpm types:
cd autogpt_platform/frontend && pnpm types(44s) - ❌ test: cd autogpt_platform/frontend && mv .env /tmp/qa-env-stash 2>/dev/null; pnpm test:unit; rc=$?; [ -f /tmp/qa-env-stash ] && mv /tmp/qa-env-stash .env; exit $rc:
cd autogpt_platform/frontend && mv .env /tmp/qa-env-stash 2>/dev/null; pnpm test:unit; rc=$?; [ -f /tmp/qa-env-stash ] && mv /tmp/qa-env-stash .env; exit $rc(368s) - ✅ build: cd autogpt_platform/frontend && pnpm build:
cd autogpt_platform/frontend && pnpm build(263s)
There was a problem hiding this comment.
❓ INCONCLUSIVE
You've hit your session limit · resets 9am (UTC)
Risk level: medium | Human review: recommended | Duration: 829s | Reviewed: 77c7827a
Specialist Reports
| Specialist | Status | Summary |
|---|---|---|
| security | You've hit your session limit · resets 9am (UTC) | |
| architect | You've hit your session limit · resets 9am (UTC) | |
| performance | You've hit your session limit · resets 9am (UTC) | |
| testing | You've hit your session limit · resets 9am (UTC) | |
| quality | You've hit your session limit · resets 9am (UTC) | |
| product | You've hit your session limit · resets 9am (UTC) | |
| discussion | You've hit your session limit · resets 9am (UTC) | |
| ui-reviewer (local) | You've hit your session limit · resets 9am (UTC) | |
| ui-reviewer (hosted) | ❌ FAIL | Variant 'hosted' failed: gh auth login failed: error validating token: Get "https://api.github.com/": net/http: TLS handshake timeout |
|
Quality Checks
- ✅ lint: cd autogpt_platform/frontend && pnpm lint:
cd autogpt_platform/frontend && pnpm lint(72s) - ✅ lint: cd autogpt_platform/backend && poetry run lint:
cd autogpt_platform/backend && poetry run lint(90s) - ✅ typecheck: cd autogpt_platform/frontend && pnpm types:
cd autogpt_platform/frontend && pnpm types(42s) - ❌ test: cd autogpt_platform/frontend && mv .env /tmp/qa-env-stash 2>/dev/null; pnpm test:unit; rc=$?; [ -f /tmp/qa-env-stash ] && mv /tmp/qa-env-stash .env; exit $rc:
cd autogpt_platform/frontend && mv .env /tmp/qa-env-stash 2>/dev/null; pnpm test:unit; rc=$?; [ -f /tmp/qa-env-stash ] && mv /tmp/qa-env-stash .env; exit $rc(355s) - ✅ build: cd autogpt_platform/frontend && pnpm build:
cd autogpt_platform/frontend && pnpm build(259s)
|
/review |
There was a problem hiding this comment.
📋 Automated Review — PR #13603
PR #13603 — feat(backend): resend org invitation — fresh token + extended TTL
Author: ntindle | Files: 4
🎯 Verdict: APPROVE
PR Description Quality
✅ Has Why + What + How — the ticket (SECRT-2475), the dead-end being fixed (revoke-and-retype), the token-rotation + TTL mechanics, and the include_expired list flag are all documented. The email-delivery and rate-limit gaps are called out as explicit TODOs.
What This PR Does
Previously, once an org invitation expired or the emailed link went stale, an admin had no recovery path except revoking it and re-typing the invitee's address. This PR adds an admin-gated POST /api/orgs/{org_id}/invitations/{invitation_id}/resend that rotates the invite token, clears tokenHash, re-bases the expiry to now+7 days, and prunes any teams deleted since the original invite. It also adds include_expired=true to the list endpoint so lapsed invitations become visible and recoverable. Backend-only; the fourth file is a regenerated openapi.json.
Specialist Findings
🛡️ Security ✅ — Auth gating (MANAGE_MEMBERS + _verify_org_path), no org enumeration (uniform 404), TOCTOU closed via compare-and-swap update_many re-asserting acceptedAt/revokedAt IS NULL, and cross-org team leak prevented by re-filtering on orgId. No secret leakage in logs. All findings are informational.
🔵 Token minted with uuid4() rather than secrets.token_urlsafe(32) (invitation_routes.py:195) — CSPRNG-backed in CPython, not exploitable, matches schema default.
🟡 No rate limiting on resend (invitation_routes.py:228) — acknowledged TODO; only a live vector once email delivery ships.
🏗️ Architecture ✅ — resend_invitation mirrors create/revoke exactly; _get_org_invitation/_reject_if_not_pending extraction removes prior copy-paste in revoke_invitation. CAS concurrency model and additive include_expired (backward-compatible) are sound.
🟠 Broken test cross-reference: comment at invitation_resend_test.py:127 cites test_resend_reads_back_the_row_it_wrote, which does not exist (real test is test_resend_reads_the_row_back_by_id at :142).
⚡ Performance ✅ — Admin-gated, low-frequency; no N+1 (_surviving_team_ids is one indexed IN query), CAS is a single indexed update. Resend does 4 sequential round trips — acceptable for a rare admin action.
🟡 include_expired=true drops the expiresAt filter and returns every unaccepted/unrevoked invite with no take/pagination (invitation_routes.py:138); expired rows are never GC'd, so the set grows unbounded per org.
🧪 Testing ✅/update_many payload/where-clause, token rotation (!= "tok-old"), tokenHash is None, TTL boundary, CAS shape, and the full 403/404 matrix. Two reachable branches in the concurrency-critical path are uncovered (see Should Fix).
🟠 "changed concurrently; retry" 400 branch (invitation_routes.py:213) and post-update refreshed is None 404 branch (invitation_routes.py:221) have no coverage.
📖 Quality ✅ — Readability grade A: precise naming (_surviving_team_ids, updated_count), justified concurrency comments placed exactly where needed, self-documenting tests.
🔵 _surviving_team_ids is a forward reference defined after its only caller (invitation_routes.py:236); re-read at :226 duplicates _get_org_invitation logic.
📦 Product ✅/feat(backend)-scoped PR, consistent with the existing create path.
🔵 "Invitation was revoked" 400 (invitation_routes.py:52) gives the admin no next step.
📬 Discussion ✅ — 46/46 CI checks green, patch coverage 97.74%, MERGEABLE, no live conflicts. 9/11 review threads resolved; author diligently addressed the CAS race and openapi-regeneration feedback. Two open threads: a confirmed false-positive Sentry HIGH (claims update_many returns BatchPayload — it returns int in prisma-client-py, same pattern as library/db.py:963), and a real minor openapi operationId naming issue. No human approval on record yet.
🟠 openapi operationId "postV2Resend invitation" contains a space + duplicated tags (openapi.json ~:9721) → awkward generated Orval hook name.
🔎 QA ✅ — Verified end-to-end over HTTP against a live DB: all 12 scenarios passed (token rotation, expired-invite recovery, include_expired toggle, team pruning, and the full 400/401/403/404 matrix). Rejected states (accepted/revoked/cross-org) performed no write. No exceptions in server logs from the endpoints.
🟠 Should Fix
- Untested
"changed concurrently; retry"400 branch (invitation_routes.py:213) — fires whenupdate_manyreturns 0 but the re-read is still pending; the existing race test only covers the concurrent-accept path. Add: update returns 0, re-read still pending → assert 400 with "concurrently". (Flagged by: testing) - Untested post-update
refreshed is None404 branch (invitation_routes.py:221) — row deleted between update and read-back; distinct from the first-read missing case. Add: update returns 1, re-read returnsNone→ assert 404. (Flagged by: testing) - Broken test cross-reference comment (
invitation_resend_test.py:127) — cites a nonexistent test name; repoint totest_resend_reads_the_row_back_by_id. (Flagged by: architect) - openapi
operationIdhas a space + duplicated tags (openapi.json~:9721) — add an explicitoperation_idto the route decorator and dedupe tags, then regenerate. Affects frontend Orval hook naming. (Flagged by: discussion, quality — 2 specialists)
🟡 Nice to Have
- Bound the
include_expiredlist (invitation_routes.py:138) — addtake=100or cursor pagination; expired invites are never garbage-collected. (performance) - Weak list assertion (
invitation_resend_test.py:328) — assert"gt" in where["expiresAt"], not just key presence. (testing) - Actionable revoked-error copy (
invitation_routes.py:52) — e.g. "Invitation was revoked; create a new invitation instead." (product)
🔵 Nits
- Token generation (
invitation_routes.py:195) —secrets.token_urlsafe(32)overuuid4()to signal credential intent. (security) - Helper ordering (
invitation_routes.py:236) — move_surviving_team_idsabove its caller. (quality) - Re-read duplication (
invitation_routes.py:226) — reuse_get_org_invitationfor the final load. (quality)
Note on the open Sentry thread
The Sentry HIGH flagging updated_count == 0 as always-False (because update_many supposedly returns BatchPayload) is a verified false positive — prisma-client-py's update_many returns int, and existing repo code (library/db.py:963,1006) uses the identical comparison. No code change needed; the thread should be resolved/dismissed to avoid misleading future readers.
Human Review Needed
YES — this change handles bearer-credential rotation and sits on the org permission/trust boundary (MANAGE_MEMBERS-gated token minting). Per policy, changes to how invitation credentials are generated and to authorization gating warrant a human sign-off; note that no human approval exists on the PR yet and the last automated pass was inconclusive.
Risk Assessment
Merge risk: LOW | Rollback: EASY (additive endpoint + backward-compatible list flag; no schema migration, revertable in one commit)
CI Status
GitHub CI (per PR discussion): ✅ 46/46 checks pass, patch coverage 97.74%, MERGEABLE.
Local harness: lint (frontend + backend), typecheck, and build all passed. The pnpm test:unit (frontend Vitest) run failed locally — the only frontend change in this PR is the regenerated openapi.json, and GitHub CI ran the same suite green on this head SHA, so this is environment skew in the review sandbox, not a real regression.
UI Testing — Variant Results
✅ local: Resend endpoint verified end-to-end over HTTP — token rotation, expired-invite recovery, list include_expired toggle, team pruning, and the full 400/401/403/404 matrix all behave exactly as specified with no writes on rejected states.
✅ hosted: Resend endpoint works end-to-end: token rotation, TTL extension, expired-invite recovery, include_expired listing, team pruning, and all error paths (401/404/400/cross-org 404) verified live with matching DB state.
- low: Resend has no rate limit or minimum-interval per invite; each call rotates the token and (once email is wired) would send a fresh link, enabling email-bombing of the invitee.
| invitation = await _get_org_invitation(org_id, invitation_id) | ||
| _reject_if_not_pending(invitation) | ||
|
|
||
| new_token = str(uuid4()) |
There was a problem hiding this comment.
🤖 🟢 low (security/token-entropy)
Invitation bearer token is minted with str(uuid4()). While uuid4 is CSPRNG-backed in CPython and matches the schema default, a UUID is an identifier type being used as a security credential.
Suggestion: Use secrets.token_urlsafe(32) for the rotated token to signal intent and increase entropy; consider aligning the schema default too.
| raise NotFoundError(f"Invitation {invitation_id} not found") | ||
|
|
||
| # TODO: Send email via Postmark with invitation link (same gap as create). | ||
| # Rate-limit resends (min-interval / per-invite cap) as part of that work — |
There was a problem hiding this comment.
🤖 🟢 low (security/rate-limiting)
The resend endpoint has no rate limiting. Currently low risk (admin-gated, email delivery not yet wired), but becomes an email-bombing vector once Postmark delivery ships.
Suggestion: Ship a per-invite min-interval and/or per-invite resend cap in the same change that wires up email delivery, as the TODO notes.
| where={"id": invitation_id, "acceptedAt": None, "revokedAt": None}, | ||
| data={ | ||
| "token": new_token, | ||
| "tokenHash": None, |
There was a problem hiding this comment.
🤖 🟢 low (security/secret-storage)
Resend persists the token in plaintext and nulls the unused tokenHash column. This mirrors pre-existing create/accept behavior, but a plaintext invite token is directly usable if the DB is compromised, and this line would silently clear any future hash-based verification.
Suggestion: Longer term, store only a hash of the invitation token (verify hash on accept) rather than the plaintext value; at minimum document why tokenHash is retained but unused.
| # The response body is the whole point of the endpoint: it must carry | ||
| # the rotated token, not the stale pre-update one. (That the token | ||
| # returned is the one this request minted is covered by | ||
| # test_resend_reads_back_the_row_it_wrote.) |
There was a problem hiding this comment.
🤖 🟡 medium (architect/comment-durability)
Comment references test 'test_resend_reads_back_the_row_it_wrote', which does not exist in the file; nearest real tests are test_resend_reads_the_row_back_by_id (:142) and test_resend_superseded_by_concurrent_resend_still_succeeds (:158). A future reader searching for the cited coverage will find nothing.
Suggestion: Point the parenthetical at an existing test, e.g. 'covered by test_resend_reads_the_row_back_by_id.'
| } | ||
| if not include_expired: | ||
| where["expiresAt"] = {"gt": datetime.now(timezone.utc)} | ||
| invitations = await prisma.orginvitation.find_many( |
There was a problem hiding this comment.
🤖 🟢 low (performance/unbounded-query)
With include_expired=true the expiresAt filter is dropped, so list_invitations returns every unaccepted/unrevoked invitation for the org with no take/pagination. Expired invitations are never cleaned up, so this set grows without bound for orgs that repeatedly let invites lapse, inflating query time, memory, and response-mapping cost.
Suggestion: Add a take limit (e.g. take=100) or cursor pagination to the find_many query so worst-case result size is bounded, especially on the include_expired path.
| if refreshed is None: | ||
| raise NotFoundError(f"Invitation {invitation_id} not found") | ||
|
|
||
| # TODO: Send email via Postmark with invitation link (same gap as create). |
There was a problem hiding this comment.
🤖 🟡 medium (product/feature-completeness)
The user-facing purpose of 'resend' is that the invitee receives the invitation email again, but email delivery is still a TODO. The endpoint only rotates the token and returns it in the API response, with no mechanism to actually deliver the new link to the invitee. Until Postmark is wired, an admin cannot complete the recovery flow this endpoint is meant to enable.
Suggestion: Track that SECRT-2475 remains open until email delivery + frontend wiring land; consider gating the endpoint behind the delivery work, or clearly document that the returned token must be surfaced to the admin in the UI as an interim delivery path.
| if invitation.acceptedAt is not None: | ||
| raise HTTPException(400, detail="Invitation already accepted") | ||
| if invitation.revokedAt is not None: | ||
| raise HTTPException(400, detail="Invitation was revoked") |
There was a problem hiding this comment.
🤖 🟢 low (product/error-copy)
The 'Invitation was revoked' 400 message gives the admin no next step, even though the intended recovery path (create a new invitation) is known. This risks reproducing the same dead-end confusion the ticket aims to remove.
Suggestion: Make the message actionable, e.g. 'Invitation was revoked; create a new invitation instead.'
| "/api/orgs/{org_id}/invitations/{invitation_id}/resend": { | ||
| "post": { | ||
| "tags": ["v2", "orgs", "invitations", "orgs", "invitations"], | ||
| "summary": "Resend invitation", |
There was a problem hiding this comment.
🤖 🟢 low (discussion/openapi-codegen)
Unresolved review thread (autogpt-pr-reviewer): the resend route's operationId is 'postV2Resend invitation' (contains a space) and the tags array is duplicated (['v2','orgs','invitations','orgs','invitations']), which yields an awkward generated Orval hook name.
Suggestion: Add an explicit operation_id (e.g. resend_invitation) to the @org_router.post decorator in invitation_routes.py and dedupe the tags list, then regenerate openapi.json.
| + timedelta(days=INVITATION_TTL_DAYS), | ||
| }, | ||
| ) | ||
| if updated_count == 0: |
There was a problem hiding this comment.
🤖 🟢 low (discussion/unresolved-false-positive)
Unresolved Sentry thread claims 'updated_count == 0' is broken because update_many returns a BatchPayload. This is a false positive: prisma-client-py update_many returns int (root_selection=['count']), and existing code (library/db.py:963,1006) uses the same integer comparison. The code is correct but the open thread is misleading.
Suggestion: Reply to and resolve/dismiss the Sentry thread noting update_many returns an int in prisma-client-py; no code change required.
| surviving = [tid for tid in invitation.teamIds if tid in valid_ids] | ||
| dropped = [tid for tid in invitation.teamIds if tid not in valid_ids] | ||
| if dropped: | ||
| logger.warning( |
There was a problem hiding this comment.
🤖 🟢 low (ui-reviewer/abuse-hardening)
Resend has no rate limit or minimum-interval per invite; each call rotates the token and (once email is wired) would send a fresh link, enabling email-bombing of the invitee.
Suggestion: Add a per-invitation min-interval / resend cap alongside the Postmark email wiring noted in the existing TODO.
| assert "revoked" in resp.json()["detail"].lower() | ||
| self.prisma.orginvitation.update_many.assert_not_called() | ||
|
|
||
| def test_resend_invitation_from_other_org_not_found(self): |
There was a problem hiding this comment.
🤖 🟡 Nice to Have: No test covers _verify_org_path on resend — i.e. path org_id differing from ctx.org_id. create_invitation and revoke_invitation each have exactly that regression test (routes_test.py:2554, :2569, added under the Bug: invitation routes missing _verify_org_path header), so the coverage is asymmetric for the one new route that needs it.
If the _verify_org_path(ctx, org_id) call at invitation_routes.py:191 is dropped in a refactor, this suite stays green while an admin of org A can POST /api/orgs/B/invitations/{id}/resend and mint a live token for org B's invitation — the invitation lookup is scoped by the path org id while the permission check reads ctx. This test varies the invitation's org, not the path's, so it doesn't cover that. (flagged by: Claude)
| """ | ||
| _verify_org_path(ctx, org_id) | ||
| invitation = await _get_org_invitation(org_id, invitation_id) | ||
| _reject_if_not_pending(invitation) |
There was a problem hiding this comment.
🤖 🟡 Nice to Have: Resend revives a pending invitation of any age with its original privilege flags, and takes no request body — so the admin clicking Resend never restates isAdmin / isBillingManager / teamIds. Combined with the new include_expired flag, which exists precisely to surface long-dead rows, an 18-month-old unaccepted invite carrying isAdmin=True (from a previous admin regime) can be resurrected with 7 fresh days. The only redemption gate is the email match, so whoever controls that mailbox today gets org admin.
Consider refusing resend when createdAt is older than some multiple of INVITATION_TTL_DAYS, forcing a deliberate re-create where the privileges are restated. (flagged by: Claude)
| updated_count = await prisma.orginvitation.update_many( | ||
| where={"id": invitation_id, "acceptedAt": None, "revokedAt": None}, | ||
| data={ | ||
| "token": new_token, |
There was a problem hiding this comment.
🤖 🟡 Nice to Have: A resend leaves no audit trail. invitedByUserId is untouched, so after a resend the invitation still names the original inviter, and OrgInvitation has no lastSentAt / resendCount column — nothing records who re-issued the credential or when.
Those same missing fields are what the rate-limit TODO below would need, so honoring it will require a schema migration rather than just handler logic. Worth capturing now while the model is being touched. (flagged by: Claude)
| return [] | ||
|
|
||
| teams = await prisma.team.find_many(where={"id": {"in": invitation.teamIds}}) | ||
| valid_ids = {t.id for t in teams if t.orgId == invitation.orgId} |
There was a problem hiding this comment.
🤖 🟡 Nice to Have: _surviving_team_ids filters on existence and orgId but not on Team.archivedAt, so an archived team survives the prune — despite the docstring's claim that pruning "keeps the stored invitation honest about what it confers."
list_teams filters archivedAt: None (team_db.py:49) and add_team_member doesn't check it either, so accepting a resent invite creates a TeamMember row in a team the user can never see. create_invitation has the same gap, so the fix probably belongs in both places. (flagged by: Claude)
77c7827 to
39d74ec
Compare
…CRT-2475) Pending (including expired) invitations get a rotated token and a new 7-day expiry; the previously emailed link stops working on resend. Accepted or revoked invitations are rejected with 400. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm3mCG9okfdGtAXtFaDF9A
…th org-UI stack routes_test.py is heavily modified by the in-flight org-UI stack; appending there made the rollup eject this PR on a test-file conflict. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move get_request_context and org_router from the local _client import to top-level imports, per the backend top-level-imports guideline. Co-Authored-By: Claude Opus <noreply@anthropic.com>
…urate Addresses review feedback on the resend endpoint: - Regenerate openapi.json for the new /resend route (fixes red `check API types` CI) plus the new list query param. - Add `include_expired` to `list_invitations`. The endpoint filtered `expiresAt > now`, so an admin could never obtain the invitation_id of an expired invite -- the resend feature's primary use case was unreachable end-to-end. Defaults to false, so existing clients are unchanged. - Close the TOCTOU window between the state read and the write. `update()` only accepts a unique WHERE in prisma-client-python, so the rotation now goes through `update_many()` with `acceptedAt`/`revokedAt` re-asserted in the WHERE clause, and reads the row back by the freshly minted token. A concurrent accept/revoke now yields 400 instead of a 200 with a live token. - Re-validate teamIds on resend: teams deleted since the invite was created are pruned (and logged) instead of silently promising access that accept can no longer grant. - Extract the shared lookup+org-match guard into `_get_org_invitation`, reused by `revoke_invitation`. - Tests: assert the response body carries the rotated token (was never checked), cover `find_unique -> None`, the CAS where-clause, the lost-race path, team pruning, and both `include_expired` modes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A second concurrent resend can rotate the token again between our update_many and the read-back. Looking up the token this request minted would then find nothing and raise NotFoundError -> 404, even though the invitation exists and was just rotated. Reading by id returns the committed state at or after our own write, so the returned token is never the stale pre-update one, and a double resend hands back a live token instead of a spurious 404. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…sponses - Register the production NotFoundError -> 404 mapping on the test app so the cross-org and missing-invitation cases assert the status callers actually receive, instead of an opaque 500 from an unhandled exception. Removes the raise_server_exceptions bypass. - Declare 400/403/404 in the resend route's OpenAPI responses so the generated client knows about the documented failure modes; regenerate openapi.json. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… docstring - Add a pytest-snapshot assertion over the InvitationCreateResponse body so drift in any field beyond the token is caught. The token (a bearer credential) and the two timestamps are excluded from the snapshot per TESTING.md's guidance on sensitive and volatile data, and are asserted explicitly instead. - Correct _expect_successful_resend's docstring, which still described the read-back as keying on the minted token. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
39d74ec to
4f851e1
Compare
Why
SECRT-2475 (org walkthrough feedback, item 5 remainder): a pending invitation's only management action is Revoke. If the invite email is lost or the 7-day token expires, the flow dead-ends — an expired invitation even disappears from the list endpoint, so the admin's only path is revoke + retype the address.
What
POST /api/orgs/{org_id}/invitations/{invitation_id}/resend(admin-gated viaMANAGE_MEMBERS, same as create/revoke):tokenHash, and a fresh 7-day expiry.How
Follows the existing org-scoped invitation route conventions (
_verify_org_path,NotFoundErrorfor 404s, permission dep). Response reusesInvitationCreateResponseso the caller gets the fresh token/link like on create.Testing
6 new tests in
routes_test.py(TestInvitationResend): token rotation + TTL extension asserted on the update payload, expired-pending success, accepted/revoked rejection with no write, cross-org 404 pattern, and 403 for non-admin members. 13/13 with the existing acceptance suite; pyright clean.Checklist
MANAGE_MEMBERS-gated +_verify_org_path; invitation looked up by id and org-matched before any write🤖 Generated with Claude Code
https://claude.ai/code/session_01Jm3mCG9okfdGtAXtFaDF9A
Note
Medium Risk
Changes org invitation token lifecycle and listing (bearer tokens, cross-org 404 semantics); mitigated by
MANAGE_MEMBERS, CAS updates, and explicit rejection of accepted/revoked invites.Overview
Adds
POST /api/orgs/{org_id}/invitations/{invitation_id}/resendso admins withMANAGE_MEMBERScan refresh a pending invite (including expired ones): new UUID token, clearedtokenHash, fresh 7-dayexpiresAt, andInvitationCreateResponsewith the current row. Token rotation invalidates older emailed links.The update uses
update_manywith a pending-state WHERE (compare-and-swap) so concurrent accept/revoke cannot be overwritten; on a lost race it re-reads and returns 400/404. The handler re-fetches by invitation id (not the minted token) so overlapping resends still return 200 with a valid token.teamIdsare pruned on resend if teams were deleted or belong to another org.List invitations gains optional
include_expired=true(default still hides expired) so admins can discover lapsed invites to resend. Revoke/list lookup logic is centralized in_get_org_invitationand_reject_if_not_pending.Adds HTTP contract tests in
invitation_resend_test.py(concurrency, permissions, team pruning, list behavior) plus an OpenAPI snapshot for the resend response shape. Email delivery and resend rate limits remain TODO like create.Reviewed by Cursor Bugbot for commit 4f851e1. Bugbot is set up for automated code reviews on this repo. Configure here.